package pong.game.open_gl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import pong.game.Data;
import pong.game.open_gl.utils.RawResourceReader;
import pong.game.open_gl.utils.ShaderHelper;
import pong.general.R;
import pong.mathematic.Engine;
import pong.mathematic.Vector3D;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.os.SystemClock;
import android.util.Log;

public class MyRenderer implements android.opengl.GLSurfaceView.Renderer {

	private static final String TAG = "Renderer";
	private static final int SLEEP_TIME = 100;

	private MyGLSurfaceView parentSurface = null;
	private boolean isSleep = false;

	// new
	private final Context mActivityContext;
	/**
	 * Store the model matrix. This matrix is used to move models from object
	 * space (where each model can be thought of being located at the center of
	 * the universe) to world space.
	 */
	private final float[] mModelMatrix = new float[16];

	/**
	 * Store the view matrix. This can be thought of as our camera. This matrix
	 * transforms world space to eye space; it positions things relative to our
	 * eye.
	 */
	private final float[] mViewMatrix = new float[16];

	/**
	 * Store the projection matrix. This is used to project the scene onto a 2D
	 * viewport.
	 */
	private final float[] mProjectionMatrix = new float[16];

	/**
	 * Allocate storage for the final combined matrix. This will be passed into
	 * the shader program.
	 */
	private final float[] mMVPMatrix = new float[16];

	/**
	 * Stores a copy of the model matrix specifically for the light position.
	 */
	private final float[] mLightModelMatrix = new float[16];

	/** Store our model data in a float buffer. */
	private final FloatBuffer mCubePositions;
	private final FloatBuffer mCubeColors;
	private final FloatBuffer mCubeNormals;
	private final FloatBuffer mCubeTextureCoordinates;

	/** This will be used to pass in the transformation matrix. */
	private int mMVPMatrixHandle;

	/** This will be used to pass in the modelview matrix. */
	private int mMVMatrixHandle;

	/** This will be used to pass in the light position. */
	private int mLightPosHandle;

	/** This will be used to pass in the texture. */
	private int mTextureUniformHandle;

	/** This will be used to pass in model position information. */
	private int mPositionHandle;

	/** This will be used to pass in model color information. */
	private int mColorHandle;

	/** This will be used to pass in model normal information. */
	private int mNormalHandle;

	/** This will be used to pass in model texture coordinate information. */
	private int mTextureCoordinateHandle;

	/** How many bytes per float. */
	private final int mBytesPerFloat = 4;

	/** Size of the position data in elements. */
	private final int mPositionDataSize = 3;

	/** Size of the color data in elements. */
	private final int mColorDataSize = 4;

	/** Size of the normal data in elements. */
	private final int mNormalDataSize = 3;

	/** Size of the texture coordinate data in elements. */
	private final int mTextureCoordinateDataSize = 2;

	/**
	 * Used to hold a light centered on the origin in model space. We need a 4th
	 * coordinate so we can get translations to work when we multiply this by
	 * our transformation matrices.
	 */
	private final float[] mLightPosInModelSpace = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };

	/**
	 * Used to hold the current position of the light in world space (after
	 * transformation via model matrix).
	 */
	private final float[] mLightPosInWorldSpace = new float[4];

	/**
	 * Used to hold the transformed position of the light in eye space (after
	 * transformation via modelview matrix)
	 */
	private final float[] mLightPosInEyeSpace = new float[4];

	/** This is a handle to our cube shading program. */
	private int mProgramHandle;

	/** This is a handle to our light point program. */
	private int mPointProgramHandle;

	public MyRenderer(Context context) {
		mActivityContext = context;
		// Define points for a cube.

		// X, Y, Z
		final float[] cubePositionData = {
				// In OpenGL counter-clockwise winding is default. This means
				// that when we look at a triangle,
				// if the points are counter-clockwise we are looking at the
				// "front". If not we are looking at
				// the back. OpenGL has an optimization where all back-facing
				// triangles are culled, since they
				// usually represent the backside of an object and aren't
				// visible anyways.

				// Front face
				-1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f,
				-1.0f,
				1.0f,
				1.0f,
				1.0f,
				1.0f,

				// Right face
				1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
				-1.0f,
				1.0f,
				1.0f,
				-1.0f,

				// Back face
				1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
				-1.0f,
				1.0f,
				-1.0f,

				// Left face
				-1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f,
				-1.0f, 1.0f,
				1.0f,

				// Top face
				-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
				1.0f, -1.0f,

				// Bottom face
				1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
				-1.0f, -1.0f, -1.0f, };

		// R, G, B, A
		final float[] cubeColorData = {
				// Front face (red)
				1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
				0.0f, 0.0f, 1.0f,
				1.0f,
				0.0f,
				0.0f,
				1.0f,

				// Right face (green)
				0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
				1.0f, 0.0f, 1.0f, 0.0f,
				1.0f,
				0.0f,
				1.0f,

				// Back face (blue)
				0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
				0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
				1.0f,
				1.0f,

				// Left face (yellow)
				1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
				1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
				1.0f,

				// Top face (cyan)
				0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
				1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,

				// Bottom face (magenta)
				1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,
				0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f };

		// X, Y, Z
		// The normal is used in light calculations and is a vector which points
		// orthogonal to the plane of the surface. For a cube model, the normals
		// should be orthogonal to the points of each face.
		final float[] cubeNormalData = {
				// Front face
				0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
				0.0f,
				1.0f,
				0.0f,
				0.0f,
				1.0f,

				// Right face
				1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
				0.0f,
				1.0f,
				0.0f,
				0.0f,

				// Back face
				0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f,
				0.0f,
				0.0f,
				-1.0f,

				// Left face
				-1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f,
				0.0f,
				0.0f,

				// Top face
				0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
				0.0f,

				// Bottom face
				0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
				-1.0f, 0.0f };

		// S, T (or X, Y)
		// Texture coordinate data.
		// Because images have a Y axis pointing downward (values increase as
		// you move down the image) while
		// OpenGL has a Y axis pointing upward, we adjust for that here by
		// flipping the Y axis.
		// What's more is that the texture coordinates are the same for every
		// face.
		final float[] cubeTextureCoordinateData = {
				// Front face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,

				// Right face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,

				// Back face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,

				// Left face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,

				// Top face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,

				// Bottom face
				0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f };

		// Initialize the buffers.
		mCubePositions = ByteBuffer.allocateDirect(cubePositionData.length * mBytesPerFloat)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		mCubePositions.put(cubePositionData).position(0);

		mCubeColors = ByteBuffer.allocateDirect(cubeColorData.length * mBytesPerFloat).order(ByteOrder.nativeOrder())
				.asFloatBuffer();
		mCubeColors.put(cubeColorData).position(0);

		mCubeNormals = ByteBuffer.allocateDirect(cubeNormalData.length * mBytesPerFloat).order(ByteOrder.nativeOrder())
				.asFloatBuffer();
		mCubeNormals.put(cubeNormalData).position(0);

		mCubeTextureCoordinates = ByteBuffer.allocateDirect(cubeTextureCoordinateData.length * mBytesPerFloat)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		mCubeTextureCoordinates.put(cubeTextureCoordinateData).position(0);
	}

	protected String getVertexShader() {
		return RawResourceReader.readTextFileFromRawResource(mActivityContext, R.raw.per_pixel_vertex_shader);
	}

	protected String getFragmentShader() {
		return RawResourceReader.readTextFileFromRawResource(mActivityContext, R.raw.per_pixel_fragment_shader);
	}

	public void onDrawFrame(GL10 gl) {

		if (!isSleeping()) {

			Engine.getInstance().run();

			//
			//
			//
			//
			//
			//
			//

			// new
			{
				GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

				// Do a complete rotation every 10 seconds.
				long time = SystemClock.uptimeMillis() % 10000L;
				float angleInDegrees = (360.0f / 10000.0f) * ((int) time);

				// Set our per-vertex lighting program.
				GLES20.glUseProgram(mProgramHandle);

				// Set program handles for cube drawing.
				mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_MVPMatrix");
				mMVMatrixHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_MVMatrix");
				mLightPosHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_LightPos");
				mTextureUniformHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_Texture");
				mPositionHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_Position");
				mColorHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_Color");
				mNormalHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_Normal");
				mTextureCoordinateHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_TexCoordinate");

				updateCameraPosition();

				// Set the active texture unit to texture unit 0.
				GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

				// Tell the texture uniform sampler to use this texture in the
				// shader by binding to texture unit 0.
				GLES20.glUniform1i(mTextureUniformHandle, 0);

				// Calculate position of the light. Rotate and then push into
				// the distance.
				Matrix.setIdentityM(mLightModelMatrix, 0);
				Matrix.translateM(mLightModelMatrix, 0, 0.0f, 0.0f, -5.0f);
				Matrix.rotateM(mLightModelMatrix, 0, angleInDegrees, 0.0f, 1.0f, 0.0f);
				Matrix.translateM(mLightModelMatrix, 0, 0.0f, 0.0f, 2.0f);

				Matrix.multiplyMV(mLightPosInWorldSpace, 0, mLightModelMatrix, 0, mLightPosInModelSpace, 0);
				Matrix.multiplyMV(mLightPosInEyeSpace, 0, mViewMatrix, 0, mLightPosInWorldSpace, 0);

				Data.getInstance().render(gl, mModelMatrix, mMVPMatrix, mViewMatrix, mProjectionMatrix, mProgramHandle,
						mLightPosInEyeSpace);

				// Draw a point to indicate the light.
				GLES20.glUseProgram(mPointProgramHandle);

				drawLight();
			}

			// RENDER !!!
			parentSurface.requestRender();

			// Log.v(TAG, "Render");
		} else {
			try {
				Thread.sleep(SLEEP_TIME);
				// Log.v(TAG, "Sleep");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Draws a point representing the position of the light.
	 */
	private void drawLight() {
		final int pointMVPMatrixHandle = GLES20.glGetUniformLocation(mPointProgramHandle, "u_MVPMatrix");
		final int pointPositionHandle = GLES20.glGetAttribLocation(mPointProgramHandle, "a_Position");

		// Pass in the position.
		GLES20.glVertexAttrib3f(pointPositionHandle, mLightPosInModelSpace[0], mLightPosInModelSpace[1],
				mLightPosInModelSpace[2]);

		// Since we are not using a buffer object, disable vertex arrays for
		// this attribute.
		GLES20.glDisableVertexAttribArray(pointPositionHandle);

		// Pass in the transformation matrix.
		Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mLightModelMatrix, 0);
		Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
		GLES20.glUniformMatrix4fv(pointMVPMatrixHandle, 1, false, mMVPMatrix, 0);

		// Draw the point.
		GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);
	}

	public void onSurfaceChanged(GL10 gl, int width, int height) {

		Log.i(TAG, "onSurfaceChanged");

		//
		//
		//
		//
		//
		//
		//
		//
		//

		// new
		{
			// Set the OpenGL viewport to the same size as the surface.
			GLES20.glViewport(0, 0, width, height);

			// Create a new perspective projection matrix. The height will stay
			// the same
			// while the width will vary as per aspect ratio.
			final float ratio = (float) width / height;
			final float left = -ratio;
			final float right = ratio;
			final float bottom = -1.0f;
			final float top = 1.0f;
			final float near = 1.0f;
			final float far = 1000000.0f;

			Engine.getInstance().setViewRatio(ratio);

			Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
		}

		Data.getInstance().initSize(height, width);
	}

	public void onSurfaceCreated(GL10 gl, EGLConfig config) {

		Log.i(TAG, "Surface create");

		// old
		{
			// GLES20.glEnable(GL10.GL_TEXTURE_2D);
			// GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
			// GLES20.glClearColor(0, 0, 0, 1f);
			// GLES20.glEnable(GL10.GL_BLEND);
			// GLES20.glFrontFace(GL10.GL_CCW);
			// GLES20.glBlendFunc(GL10.GL_SRC_ALPHA,
			// GL10.GL_ONE_MINUS_SRC_ALPHA);
			// GLES20.glDisable(GL10.GL_NORMALIZE);
			// GLES20.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
			// GL10.GL_FASTEST);
		}

		// new
		{
			// Set the background clear color to gray.
			GLES20.glClearColor(0.0f, 0.4f, 0.0f, 0.0f);

			// Use culling to remove back faces.
			GLES20.glEnable(GLES20.GL_CULL_FACE);

			// Enable depth testing
			// GLES20.glEnable(GLES20.GL_DEPTH_TEST);

			GLES20.glEnable(GLES20.GL_TEXTURE_2D);
			GLES20.glEnable(GLES20.GL_BLEND);
			GLES20.glFrontFace(GLES20.GL_CCW);
			GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);

			// GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);

			// GLES20.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

			updateCameraPosition();

			final String vertexShader = getVertexShader();
			final String fragmentShader = getFragmentShader();

			final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
			final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);

			mProgramHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle, new String[] {
					"a_Position", "a_Color", "a_Normal", "a_TexCoordinate" });

			// Light point -----------
			// Define a simple shader program for our point.
			// final String pointVertexShader =
			// RawResourceReader.readTextFileFromRawResource(mActivityContext,
			// R.raw.point_vertex_shader);
			// final String pointFragmentShader =
			// RawResourceReader.readTextFileFromRawResource(mActivityContext,
			// R.raw.point_fragment_shader);
			//
			// final int pointVertexShaderHandle =
			// ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER,
			// pointVertexShader);
			// final int pointFragmentShaderHandle =
			// ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER,
			// pointFragmentShader);
			// mPointProgramHandle =
			// ShaderHelper.createAndLinkProgram(pointVertexShaderHandle,
			// pointFragmentShaderHandle,
			// new String[] { "a_Position" });

			// Load textures
			Data.getInstance().loadTextures(mActivityContext);

		}

		parentSurface.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
		Log.i(TAG, "GLSurface Render is created \"memory\"");
	}

	private void updateCameraPosition() {
		final Vector3D camPos = Data.getInstance().getCamera().getPosition();

		// Position the eye in front of the origin.
		final float eyeX = camPos.getX();
		final float eyeY = camPos.getY();
		final float eyeZ = camPos.getZ();

		// We are looking toward the distance
		final float lookX = camPos.getX();
		final float lookY = camPos.getY();
		final float lookZ = camPos.getZ() - 1;

		// Set our up vector. This is where our head would be pointing were
		// we holding the camera.
		final float upX = 0.0f;
		final float upY = 1.0f;
		final float upZ = 0.0f;

		// Set the view matrix. This matrix can be said to represent the
		// camera position.
		// NOTE: In OpenGL 1, a ModelView matrix is used, which is a
		// combination of a model and
		// view matrix. In OpenGL 2, we can keep track of these matrices
		// separately if we choose.
		Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
	}

	public void setSleep(boolean state) {

		this.isSleep = state;

		if (state) {

			// Data.getInstance().setBubblesGeneratorSleep(state);
			// Data.getInstance().suspendBitmapQueue(false);

		} else {
			// Data.getInstance().setBubblesGeneratorSleep(state);
			// Data.getInstance().resumeBitmapQueue();
			Engine.getInstance().updateTime();
			parentSurface.requestRender();
		}
	}

	public void setParentSurface(MyGLSurfaceView parent) {
		parentSurface = parent;
	}

	public boolean isSleeping() {
		return isSleep;
	}
}
